/**
 * Class: SongAnalyst
 *
 * Purpose: 
 *
 * Created: Jan 20, 2010 @ 11:42:18 AM
 * @author Ken Scott
 */
package org.openfantasia.songanalyst;

import org.openfantasia.ui.PColor;
import org.openfantasia.ui.PComponent;
import org.openfantasia.ui.PFrame;
import org.openfantasia.util.listener.*;
import org.openfantasia.util.math.Math1D;
import org.openfantasia.util.math.Math2D;
import org.openfantasia.util.math.PMath;
import ddf.minim.AudioOutput;
import ddf.minim.AudioPlayer;
import ddf.minim.Minim;
import ddf.minim.analysis.FFT;
import ddf.minim.effects.IIRFilter;
import ddf.minim.signals.SineWave;
import processing.core.PApplet;
import processing.core.PFont;

public class SongAnalyst extends PFrame {

	public static void main(String args[]) {
		PApplet.main(new String[]{"--position=100,100", SongAnalyst.class.getName()});
	}

	// LABELS

	public static final String CURSOR = "cursor";

	public static final String POWER_CURVE = "power_curve";

	public static final String CYCLE_COUNTER = "cycle_counter";

	public static final String PEAK = "peak";

	public static final String PEAK_AVERAGE = "peak_average";

	public static final String LIVE = "live";

	public static final String LIVE_AVERAGE = "live_average";

	public static final String SMOOTH_PIPE = "smooth_pipe";

	public static final String SMOOTH_AVERAGE = "smooth_average";

	public static final String FULL_AVERAGE = "full_average";

	public static final String TEST_FREQUENCY = "test_frequency";

	public static final String FULL_SMOOTH_AVERAGE = "full_smooth_average";

	public static final String RMS_SMOOTH_AVERAGE = "rms_smooth_average";

	public static final String RMS2_SMOOTH_AVERAGE = "rms2_smooth_average";

	public static final float[] STD_OCTAVES = {27.5f, 55f, 110f, 220f, 440f, 880f, 1760f, 3520f, 7540f, 15080f, 30160f};

	// SETTINGS

	String file = "bep-i-gotta-feeling.mp3";

	float fftSize = pow(2, 12);

	// FIELDS

	boolean showPeaks = false;

	boolean showPeakAvg = false;

	boolean showLive = false;

	boolean showLiveAvg = false;

	boolean showAvg = false;

	boolean showAvgAvg = false;

	boolean showSmoothAvg = true;

	boolean showSmoothAvgAvg = true;

	SineWave testSignal;

	AudioOutput audioOut;

	boolean testSignalActive = false;

	Minim minim;

	AudioPlayer player;

	FFT fft;

	ListenerManager listeners = new ListenerManager();

	IIRFilter lowPassFilter;

	IIRFilter voiceFilterHi;

	IIRFilter voiceFilterLo;

	float[] sharpenKernal1d;

	float[][] timeKernal2d;

	float baseFreq = 55f;

	float minFreq;

	float maxFreq;

	float octaveCount = 7;

	float toneCount = 12;

	float freqCount = octaveCount * toneCount;

	float testFreq;

	float vScale = 0.005f;

	int hStep = 4;

	float hScale = 1.0f / (float) hStep;

	float cycleCounter = 0;

	long endTime;

	float bufferSize = fftSize * hScale / 2.0f;

	int[] offx = new int[(int) bufferSize];

	float[] peak = new float[(int) bufferSize];

	float[] avgPower = new float[(int) bufferSize];

	float[] fullAverage = new float[(int) bufferSize];

	int band;

	float[] bandFreqs;

	float[] livePower = new float[(int) bufferSize];

	int pipeLength = 15;

	float[][] pipe = new float[pipeLength][(int) bufferSize];

	float[][] smoothPipe;

	float[] smoothAverage;

	float rms2Scale = (float) Math.sqrt(2.0);

	AltOctaveGrid octaveGrid;

	PowerCurveDisplay smoothAverageDisplay;


	public void setup() {
		super.setup();
		int iwidth = (int) bufferSize * hStep;
		int iheight = 1000;
		size(iwidth, iheight);

		addControl(new Cursor(0, 0, iwidth, iheight));

		setColor(CURSOR, PColor.RED);
		setColor(PEAK, PColor.GREEN.scale(0.75f));
		setColor(PEAK_AVERAGE, PColor.GREEN.scale(0.5f));
		setColor(LIVE, PColor.WHITE);
		setColor(LIVE_AVERAGE, PColor.WHITE.scale(0.75f));
		setColor(FULL_AVERAGE, PColor.BLUE.scale(0.75f));
		setColor(SMOOTH_AVERAGE, PColor.MAGENTA);
		setColor(FULL_SMOOTH_AVERAGE, PColor.RED.scale(0.75f));
		setColor(RMS_SMOOTH_AVERAGE, PColor.RED.scale(0.75f));
		setColor(RMS2_SMOOTH_AVERAGE, PColor.RED.scale(0.75f));

		minim = new Minim(this);

		for (int i = 0; i < (int) bufferSize; i++) {
			offx[i] = i * hStep;
		}

		sharpenKernal1d = new float[3];
		sharpenKernal1d[0] = 2.0f;
		sharpenKernal1d[1] = 1.0f;
		sharpenKernal1d[2] = 0.0f;

		timeKernal2d = new float[pipeLength][1];
		for (int i = 0; i < pipeLength; i++) {
			timeKernal2d[i][0] = (float) 1.0 / ((float) i + 1f);
		}

		player = minim.loadFile(file, (int) fftSize);
		player.play();
		endTime = player.length();

		audioOut = minim.getLineOut(Minim.STEREO);

		fft = new FFT((int) fftSize, player.sampleRate());
		fft.logAverages((int) baseFreq, (int) toneCount);
//		fft.linAverages((int) bufferSize);

//		octaveGrid = new OctaveGrid(0, 0, iwidth, iheight, 12);
		octaveGrid = new AltOctaveGrid(0, 0, iwidth, iheight, 8, 12);
		addBackground(octaveGrid);

		smoothAverageDisplay = new PowerCurveDisplay(0, 0, iwidth, iheight, vScale, getColor(SMOOTH_AVERAGE), getColor(FULL_SMOOTH_AVERAGE), getColor(RMS_SMOOTH_AVERAGE));
		addForeground(smoothAverageDisplay);
		addListener(SMOOTH_AVERAGE, smoothAverageDisplay);


		addForeground(new Stats(iwidth - 180, 20, 160, 130, createFont("SanSerif", 12)));
	}


	public void addListener(String category, Listener listener) {
		listeners.add(category, listener);
	}


	public void removeListener(String category, Listener listener) {
		listeners.remove(category, listener);
	}


	public void broadcast(String category, float value) {
		listeners.send(category, new FloatScalar(value));
	}


	public void broadcast(String category, float[] buffer) {
		listeners.send(category, new FloatArray(buffer));
	}


	public void broadcast(String category, float[][] buffer) {
		listeners.send(category, new FloatGrid(buffer));
	}


	public synchronized void mousePressed() {
		testSignalActive = true;
		testSignal = new SineWave(testFreq, 0.1f, player.sampleRate());
		testSignal.portamento(100);
		audioOut.addSignal(testSignal);

		super.mousePressed();
	}


	public synchronized void mouseReleased() {
		testSignalActive = false;
		audioOut.removeSignal(testSignal);
		testSignal = null;

		super.mouseReleased();
	}


	public void preDraw() {
		// Do the FFT on the audio stream
		fft.forward(player.mix);

		// Record the band frequencies
		if (bandFreqs == null) {
			bandFreqs = new float[(int) bufferSize];
			for (int i = 0; i < (int) bandFreqs.length; i++) {
				bandFreqs[i] = fft.indexToFreq(i);
			}
			minFreq = bandFreqs[0];
			maxFreq = bandFreqs[(int) bandFreqs.length - 1];
			
			octaveGrid.setFrequencies(bandFreqs);
			smoothAverageDisplay.setFrequencies(bandFreqs);
		}

		// Grab the current power levels
		for (int i = 0; i < (int) bufferSize; i++) {
			livePower[i] = fft.getBand(i);
		}

		// Push the power levels into a smoothing pipeline
		for (int i = pipeLength - 1; i > 0; i--) {
			System.arraycopy(pipe[i - 1], 0, pipe[i], 0, (int) bufferSize);
		}
		System.arraycopy(livePower, 0, pipe[0], 0, (int) bufferSize);
		smoothPipe = Math2D.filter(pipe, timeKernal2d);
		smoothAverage = PMath.mix(smoothPipe);
		float avg = Math1D.avg(smoothAverage);
		float rms2 = PMath.sqrt(2f) * avg;
		float rms = rms2/2.0f;

		// Find the peaks
		for (int i = 0; i < (int) bufferSize; i++) {
			if (livePower[i] > peak[i]) {
				peak[i] = livePower[i];
			}
		}

		// Keep a running average
		if (millis() < endTime) {
			cycleCounter += 1.0;
			for (int i = 0; i < (int) bufferSize; i++) {
				if (millis() < endTime) {
					avgPower[i] = ((avgPower[i] * (cycleCounter - 1)) + livePower[i]) / cycleCounter;
				}
			}
			fullAverage = Math1D.filter(avgPower, sharpenKernal1d);
		}

		// Update the test signal
		float dx = (float) mouseX / (float) width;
		float df = maxFreq - minFreq;
		testFreq = (dx * df) + minFreq;
		synchronized (this) {
			if ((testSignal != null) && (testFreq != testSignal.frequency())) {
				testSignal.setFreq(testFreq);
			}
		}

		// Let the listeners know that something(s) happened
		broadcast(SMOOTH_AVERAGE, smoothAverage);
//		broadcast(LIVE, livePower);
//		broadcast(PEAK, peak);
//		broadcast(SMOOTH_PIPE, smoothPipe);
//		broadcast(FULL_AVERAGE, fullAverage);
//		broadcast(CYCLE_COUNTER, cycleCounter);
//		broadcast(TEST_FREQUENCY, testFreq);
	}


	public void drawDynamic() {
		float px = 0, py, nx, ny;

		// Draw the peaks
		if (showPeaks) {
			float sum = 0f;
			fill(getColor(PEAK));
			noStroke();
			for (int i = 0; i < (int) bufferSize; i++) {
				float p = peak[i];
				int p2 = (int) (p * bandFreqs[i] * vScale);
				rect(offx[i], height - p2, hStep, p2);
			}

			if (showPeakAvg) {
				float avg = sum / bufferSize;
				stroke(getColor(PEAK_AVERAGE));
				line(0, height - avg, width, height - avg);
			}
		}

		// Draw the live power
		if (showLive) {
			float[] live = Math1D.scale(vScale, Math1D.mul(livePower, bandFreqs));
			float avg = PMath.avg(live) * vScale;

			fill(getColor(LIVE));
			noStroke();
			for (int i = 0; i < (int) bufferSize; i++) {
				float h = live[i];
				rect(offx[i], height - h, hStep, h);
			}

			if (showLiveAvg) {
				stroke(getColor(LIVE_AVERAGE));
				line(0, height - avg, width, height - avg);
			}
		}

		// Get the band where the cursor is sitting
		band = (int) PMath.clip(0, 511, (float) mouseX * hScale);

		// Draw the average
		if (showAvg) {
			float sum = 0f;
			fill(getColor(FULL_AVERAGE));
			noStroke();
			for (int i = 0; i < (int) bufferSize; i++) {
				float a = fullAverage[i];
				int a2 = (int) (a * bandFreqs[i] * vScale);
				sum += a2;
				rect(offx[i], height - a2, hStep, a2);
			}
		}
	}


	public void stop() {
		// always close Minim audio classes when you finish with them
		player.close();
		minim.stop();

		super.stop();
	}


	public class OctaveGrid extends PComponent {

		public static final String OCTAVE = "octave";

		public static final String TONE = "tone";

		private float minFreq;

		private float maxFreq;

		private int tonesPerOctave;

		private float[] freqOffsets;

		private float[][] toneOffsets;

		private float[] octaveOffsets;

		private float[] tones;

		private float[] octaves;

		private int baseIndex = -1;

		private float baseFreq;


		public OctaveGrid(float x, float y, float w, float h, int tonesPerOctave) {
			super(x, y, w, h);

			this.tonesPerOctave = tonesPerOctave;

			setColor(OCTAVE, PColor.GREEN);
			setColor(TONE, PColor.GREEN.scale(0.25f));
		}


		public void setFrequencies(float[] freqs) {
			minFreq = freqs[0];
			maxFreq = freqs[freqs.length - 1];

			freqOffsets = findToneOffsets(freqs);
			for (int i = 1; (i < freqs.length) && (baseIndex == -1); i++) {
				if (minFreq < freqs[i]) {
					baseIndex = i - 1;
					baseFreq = freqs[baseIndex];
				}
			}
		}


		public float[] findToneOffsets(float[] freqs) {
			float min = freqs[0];
			float max = freqs[freqs.length - 1];
			float df = getWidth() / (max - min);

			float[] result = new float[freqs.length];
			for (int i = 0; i < freqs.length; i++) {
				result[i] = df * (float) i;
			}

			return result;
		}


		public void draw() {
			if (baseIndex != -1) {
				float width = getWidth();
				float height = getHeight();
				float dFreq = width / (maxFreq - minFreq);
				float freq;

				float mark;
				for (float octaveFreq : STD_OCTAVES) {
					for (int i = 0; i < tonesPerOctave; i++) {
						stroke(getColor(TONE));
						freq = octaveFreq * pow(2, (float) i / (float) tonesPerOctave);
						mark = (freq - minFreq) * dFreq;
						line(mark, 0f, mark, height - 1);
					}

					stroke(getColor(OCTAVE));
					mark = (octaveFreq - minFreq) * dFreq;
					line(mark, 0f, mark, height - 1);
				}
			}
		}
	}

	public class AltOctaveGrid extends PComponent {

		public static final String TEXT = "text";

		public static final String OCTAVE = "octave";

		public static final String TONE = "tone";

		private float minFreq;

		private float maxFreq;

		private int octaveCount;

		private int toneCount;

		private float[] freqOffsets;

		private float[][] toneOffsets;

		private float[] octaveOffsets;

		private float[] toneX;

		private float[] octaveX;

		private int baseIndex = -1;

		private float baseFreq;


		public AltOctaveGrid(float x, float y, float w, float h, int octaveCount, int toneCount) {
			super(x, y, w, h);

			this.octaveCount = octaveCount;
			this.toneCount = toneCount;

			setColor(TEXT, PColor.WHITE);
			setColor(OCTAVE, PColor.GREEN);
			setColor(TONE, PColor.GREEN.scale(.25f, 1.0f));
			findToneOffsets();
		}


		public void setFrequencies(float[] freqs) {
			minFreq = freqs[0];
			maxFreq = freqs[freqs.length - 1];

			for (int i = 1; (i < freqs.length) && (baseIndex == -1); i++) {
				if (minFreq < freqs[i]) {
					baseIndex = i - 1;
					baseFreq = freqs[baseIndex];
				}
			}
		}


		public void findToneOffsets() {
			float width = getWidth();

			toneOffsets = new float[octaveCount][toneCount];
			octaveOffsets = new float[octaveCount];

			float octaveWidth = width / (float) octaveCount;
			float toneWidth = octaveWidth / (float) toneCount;
			for (int octave = 0; octave < octaveCount; octave++) {
				for (int tone = 0; tone < toneCount; tone++) {
					toneOffsets[octave][tone] = (octave * octaveWidth) + (tone * toneWidth);
				}

				octaveOffsets[octave] = octave * octaveWidth;
			}
		}


		public void draw() {
			if (baseIndex != -1) {
				float height = getHeight();

				float mark;
				for (int octave = 0; octave < octaveCount; octave++) {
					stroke(getColor(TONE));
					for (int tone = 0; tone < toneCount; tone++) {
						mark = toneOffsets[octave][tone];
						line(mark, 0f, mark, height - 1);
					}

					stroke(getColor(OCTAVE));
					mark = octaveOffsets[octave];
					line(mark, 0f, mark, height - 1);
				}
			}
		}
	}

	public class PowerCurveDisplay extends PComponent implements Listener {

		private float scale;

		private float[] values;

		private float avg;

		private float rms;

		private float[] frequencies;

		private float minFreq, maxFreq;

		private PFont font = createFont("SanSerif", 9);


		public PowerCurveDisplay(float x, float y, float w, float h, float scale, PColor curveC, PColor avgC, PColor rmsC) {
			super(x, y, w, h, false);
			setColor(POWER_CURVE, curveC);
			setColor(FULL_SMOOTH_AVERAGE, avgC);
			setColor(RMS_SMOOTH_AVERAGE, rmsC);
			this.scale = scale;
		}


		public void setFrequencies(float[] freqs) {
			frequencies = Math1D.copy(freqs);
			minFreq = frequencies[0];
			maxFreq = frequencies[frequencies.length - 1];
		}


		public void receive(Event event) {
			values = ((FloatArray) event).getValue();
			values = Math1D.mul(values, frequencies);
			values = Math1D.scale(scale, values);
			avg = Math1D.avg(values);
			rms = avg * PMath.sqrt(2f);
		}


		public void draw() {
			if (values != null) {
				float width = getWidth();
				float height = getHeight();

				float px = 0f, py = height, nx, ny;
				textFont(font);
				noFill();
				stroke(getColor(POWER_CURVE));
				for (int i = 0; i < values.length; i++) {
					float di = (float) i / (float) values.length;
					nx = width * di;
					ny = height - values[i];
					line(px, py, nx, ny);
//					text(""+frequencies[i], nx, ny);
					px = nx;
					py = ny;
				}

				stroke(getColor(FULL_SMOOTH_AVERAGE));
				line(0, height - avg, width, height - avg);

				stroke(getColor(RMS_SMOOTH_AVERAGE));
				line(0, height - rms, width, height - rms);
			}
		}
	}

	public class PowerLevelDisplay extends PComponent implements Listener {

		private float scale;

		private float value;

		private float minFreq, maxFreq;

		public PowerLevelDisplay(float x, float y, float w, float h, float scale, PColor lineC) {
			super(x, y, w, h, false);
			setColor(POWER_CURVE, lineC);
			this.scale = scale;
		}

		public void setFrequencies(float[] freqs) {
			minFreq = freqs[0];
			maxFreq = freqs[freqs.length - 1];
		}

		public void receive(Event event) {
			value = ((FloatScalar) event).getValue() * scale * (maxFreq - minFreq)/2f;
		}


		public void draw() {
			float width = getWidth();
			float height = getHeight();
			noFill();
			stroke(getColor(POWER_CURVE));
			line(0, height - value, width, height - value);
		}
	}

	public class Cursor extends PComponent {

		public static final String PASSIVE = "passive";

		public static final String ACTIVE = "active";


		public Cursor(float x, float y, float w, float h) {
			super(x, y, w, h, false);

			setColor(PASSIVE, PColor.RED.scale(0.75f, 0.75f));
			setColor(ACTIVE, PColor.RED);
		}


		public void draw() {
			if (isOver()) {
				float mx = mouseX();
				noStroke();
				if (isTracking()) {
					stroke(getColor(ACTIVE));
				}
				else {
					stroke(getColor(PASSIVE));
				}
				line(mx, 0, mx, getHeight());
			}
		}
	}

	public class Stats extends PComponent {

		public static final String TEXT = "text";

		private PFont font;


		public Stats(float x, float y, float w, float h, PFont font) {
			super(x, y, w, h, false);

			this.font = font;

			setColor(TEXT, PColor.WHITE);
		}


		public void draw() {
			fill(PColor.BLACK);
			stroke(PColor.WHITE);
			rect(0, 0, getWidth() - 1, getHeight() - 1);

			textFont(font);
			fill(getColor(TEXT));
			int xLoc = 10;
			int yLoc = 0;
			text("Frame Rate: " + frameRate, xLoc, yLoc += 20);
			text("Band: " + band, xLoc, yLoc += 20);
			text("Freq: " + testFreq, xLoc, yLoc += 20);
			text("Peak: " + peak[band], xLoc, yLoc += 20);
			text("Avg: " + avgPower[band], xLoc, yLoc += 20);
			text("mouseX: " + mouseX, xLoc, yLoc += 20);
		}
	}
}
